(ns cljsso.routes
  (:require
   [compojure.core :refer [defroutes GET POST ]]
   [noir.util.route :refer (def-restricted-routes)]
   [compojure.handler :as handler]
   [compojure.route :as route]
   [noir.session :as session]
   [noir.response :as r]
   [noir.cookies :as c]

   [taoensso.timbre :as timbre]

   [ring.util.anti-forgery :refer [anti-forgery-field]]
   [ring.middleware.anti-forgery :refer [*anti-forgery-token*]]
   [clojure.core.memoize :as memo]

   [selmer.filters :refer [add-filter!]]
   [ring.middleware.reload :as reload]
   [org.httpkit.server :refer [run-server]]

   [cljsso.config :refer [props]]
   [cljsso.layout :refer [render]]
   [cljsso.utils :refer [find-host]]
   [cljsso.logger :refer [wrap-request-logger]]
   [cljsso.store :refer [check-ticket gen-ticket invalidate-ticket
                         get-userinfo-from-ticket
                         init-host-keys
                         sso-clients-set
                         get-active-sessions id->client-host client-host->id id->client-logout-url
                         add-active-session]]

   [cljsso.auth :refer [auth]]

   [sparrows.cypher :refer :all]
   [sparrows.misc :refer :all]
   [org.httpkit.client :as http]))


(defonce version-id
  (rand-str 5))



(timbre/refer-timbre)


(defroutes base-routes
  (route/resources "/"))

(defn get-ticket
  [cookies]
  (get-in cookies ["ticket" :value]))


(defn get-all-hosts
  "Get all logged in client hosts for current session"
  []
  (when-let [ticket (session/get :ticket)]
    (let [{:keys [uid skey]} (get-userinfo-from-ticket ticket)
          client-ids         (get-active-sessions uid skey)]
      (map id->client-host client-ids))))

(defn get-logout-urls
  "Get logout-urls to be requested to logout this user"
  []
  (when-let [ticket (session/get :ticket)]
    (let [{:keys [uid skey]} (get-userinfo-from-ticket ticket)
          client-ids         (get-active-sessions uid skey)]
      (map id->client-logout-url client-ids))))

(def clients
  (map :host (:sso-clients props)))


(defn- logout
  [userinfo]
  (doseq [logout-url (get-logout-urls)]
    (info "Logging out" logout-url)
    (http/get (str logout-url "?uid=" (:uid userinfo) "&skey=" (:skey userinfo))
              {:insecure? (get props :insecure? true)}
              (fn logout-callback
                [{:keys [body status error]}]
                (info "Logout" logout-url " s:" status " b:" body " e:" error)))))

(defn logout-all-apps
  [ticket]
  (when-let [userinfo (get-userinfo-from-ticket ticket)]
    (logout userinfo)
    (invalidate-ticket  ticket)))


(defn gen-resp-map
  []
  {:versionid version-id
   :aff-token *anti-forgery-token*
   :aff-field [:safe (anti-forgery-field)]
   :user      (session/get :user)
   :userinfo  (session/get :userinfo)})



(defn- url-validator
  "Check if the url is from an allowed server"
  [url]
  (debug "Validating" url "in" sso-clients-set)
  (when-let [host (find-host url)]
    (contains? sso-clients-set host)))

(defn- do-logout
  [ticket]
  (logout-all-apps ticket)
  (c/put! :ticket {:value "" :path "/" :max-age 0})
  (session/clear!))

(def-restricted-routes main-routes
  ;; Display this page is user direct accesses SSO server
  (GET "/" []
       (let [userinfo (get-userinfo-from-ticket (session/get :ticket))]
         (render "home.html" (merge (gen-resp-map)
                                    {:logged-hosts (get-all-hosts) :userinfo     userinfo}))))

  (GET "/logout" {{:keys [service]} :params
                  headers :headers
                  cookies :cookies}
       (when-let [ticket (get-ticket cookies)]
         (do-logout ticket))
       (r/redirect
        (if service
          (form-decode service)
          (str "/login?service=" (get headers "referer")))))

  ;; Your routes

  )




(defn construct-redirect
  "Construct redirect url. If `url` is not in the allowed client host
  list, return to home page of SSO server"
  ([url]
   (construct-redirect url nil))
  ([url ticket]
   (if (url-validator url)
     (if ticket
       (str url (if (.contains url "&") "&" "?") "ticket=" ticket)
       url)
     "/")))


(defroutes app-routes
  ;; show login page
  ;; will redirect to service page if exists
  (GET "/login"  {{:keys [service]} :params
                  headers :headers
                  cookies :cookies
                  :as request}
       (let [ticket  (trim (get-in cookies ["ticket" :value]))]
         (if (and ticket (check-ticket ticket))
           (if service
             (do
               ;;and also add this session to active sessions
               (add-active-session ticket (form-decode service))
               (r/redirect (-> service form-decode (construct-redirect ticket))))
             (r/redirect "/"))
           ;; Instead of simply use render, we call .render here in order to disable browser caching
           (r/set-headers
            {"Cache-Control" "no-cache"}
            (.render
             (render "login.html"
                     (assoc (gen-resp-map) :service service))
             request)))))

  (POST "/login" {{:keys [username password service] } :params}
        (try

          (if-let [userinfo (auth {:uid username :password password})]
            (let [host-id (client-host->id (find-host service))
                  ticket  (gen-ticket {:uid username :host-id host-id :userinfo userinfo})]
              (c/put! :ticket ticket)
              (session/put! :ticket ticket)
              (session/put! :user username)
              (if service
                ;; TODO remove `service` and `ticket` which is already in service url
                (r/redirect (if (.contains service "?")
                              (str service "&ticket=" ticket)
                              (str service "?ticket=" ticket)))
                (r/redirect "/")))
            (render "login.html" (assoc (gen-resp-map) :service service :error "用户名或密码错误!提示:默认实现中,用户名与密码一样")))

          (catch Exception e
            (error e "Login exception: " (.getMessage e))
            (render "login.html" {:error "Server Error!"}))))


  (GET "/validate" {{:keys [ticket]} :params}
       (r/json
        (if (and ticket (check-ticket ticket))
          {:code 0}
          {:code 2 :msg "Ticket invalid!"})))

)
